/**
 * Copyright 2012 University Of Illinois
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//Export items from module and inject them into the import scope
var EXPORTED_SYMBOLS = ["OAA_WEB_ACCESSIBILITY"];

Components.utils.import("resource://gre/modules/PluralForm.jsm");

/**
 * @namespace OAA_WEB_ACCESSIBILITY
 */
var OAA_WEB_ACCESSIBILITY = OAA_WEB_ACCESSIBILITY || {};

/**
 * @namespace OAA_WEB_ACCESSIBILITY.util
 */
OAA_WEB_ACCESSIBILITY.util = OAA_WEB_ACCESSIBILITY.util || {};

/* ---------------------------------------------------------------- */
/*                      Highlight Module                            */
/* ---------------------------------------------------------------- */

/**
 * @namespace OAA_WEB_ACCESSIBILITY.util.highlightModule
 */

OAA_WEB_ACCESSIBILITY.util.highlightModule = {

  /**
   * @function initHighlight
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Initialize helper objects
   */
  initHighlight: function() {

    var stringBundle = window.document.getElementById("ID_STRINGBUNDLE_HIGHLIGHT");

    this.STRINGS = {
      violations:   stringBundle.getString('violations'),
      warnings:     stringBundle.getString('warnings'),
      manualChecks: stringBundle.getString('manualChecks'),
      passes:       stringBundle.getString('passes'),
      hidden:       stringBundle.getString('hidden'),
      offScreen:    stringBundle.getString('offScreen'),
      notVisible:   stringBundle.getString('notVisible'),
      noResults:    stringBundle.getString('noResults')
    };

    this.STYLES = {
      violations:   'border: medium solid red; border-collapse: separate;',
      warnings:     'border: medium solid orange; border-collapse: separate;', 
      manualChecks: 'border: medium dotted red; border-collapse: separate;',
      passes:       'border: medium solid green; border-collapse: separate;', 
      hidden:       'border: medium dotted grey; border-collapse: separate;',
      result:       'border: medium solid black; border-collapse: separate;',
      offScreen:    'top: 2em; left: 2em; width:40%; padding:10px; margin:2px; background-color: white; color: black; font-size:120%; position:fixed; z-index: 100; opacity: 0.85;',
      nodeResults:  'border:3px solid grey;'
    };
  },

  /**
   * @function setHighlightPreferences
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Set module properties that control highlighting according to
   *       user preferences
   *
   * @param {Boolean} show_element_manual_check
   * @param {Boolean} show_page_manual_check
   * @param {Boolean} show_pass
   * @param {Boolean} show_hidden
   */
  setHighlightPreferences: function (show_element_manual_check,
                                     show_page_manual_check,
                                     show_pass,
                                     show_hidden) {

    this.show_element_manual_check = show_element_manual_check;
    this.show_page_manual_check    = show_page_manual_check;
    this.show_pass                 = show_pass;
    this.show_hidden               = show_hidden;
  },

  /**
   * @function scopePageCount
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Calculate the number of node results in the list that are from rules
   *       with a scope property set to page.
   *
   * @param {Array} node_results - List of node result objects
   *
   * @return {Number} Number of node results with page scope
   */
  scopePageCount: function(node_results) {
    var count = 0;
    for (var i = 0; i < node_results.length; i++)
      if (node_results[i].isScopePage()) count++;
    return count;
  },

  /**
   * @function scopeElementCount
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Calculate the number of node results in the list that are from rules
   *       with a scope property set to element.
   *
   * @param {Array} node_results - List of node result objects
   *
   * @return {Number} Number of node results with element scope
   *
   */
  scopeElementCount: function (node_results) {
    var count = 0;
    for (var i = 0; i < node_results.length; i++)
      if (node_results[i].isScopeElement()) count++;
    return count;
  },

  /**
   * @function highlightCacheItems
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Highlight a single cache item or a list of cache items. Uses worst
   *       node result of each cache item to style highlight.
   *
   * @param {Object} document - the target DOM
   * @param {Object} item - cache item object or a list of cache item objects
   *        (NOTE: can be simple or set of dom element) to highlight
   */
  highlightCacheItems: function (document, item) {

    var VISIBILITY = OpenAjax.a11y.VISIBILITY;
    this.removeHighlight(document);
    if (typeof item !== 'object') return;

    var cache_items = [];

    if (typeof item.dom_element === 'object' || typeof item.type === 'number') {
      cache_items.push(item)
    }
    else {
      cache_items = item;
    }

    var cache_items_len = cache_items.length;
    var off_screen_elements = [];

    var v = 0;
    var w = 0;
    var pp = 0;
    var m = 0;
    var hh = 0;

    // OpenAjax.a11y.logger.debug("HIGHLIGHT CACHE ITEMS 2: " + cache_items.length);

    for (var i = 0; i < cache_items_len; i++) {
    
      if (typeof cache_items[i] !== 'object') continue;

      var cache_item = cache_items[i];

      var de = (typeof cache_item.dom_element === 'object') ?
               cache_item.dom_element : cache_item;

      var style = this.getWorstResultValueStyle(de);

      var style_de = (de.type === Node.TEXT_NODE) ?
                     de.parent_element : de;

      var node     = style_de.node;
      var tag_name = style_de.tag_name;

      // OpenAjax.a11y.logger.debug("CACHE ITEM: " + cache_item +
      //   " onscreen: " + style.is_visible_onscreen + " style: " + style);

      if (node) {

        // check if the node is off screen or hidden from assistive technologies
        if (style_de.computed_style.is_visible_onscreen === VISIBILITY.HIDDEN) {

          if (de.rules_violations.length ||
              de.rules_warnings.length ||
              (this.show_page_manual_check && this.scopePageCount(de.rules_manual_checks)) ||
              (this.show_element_manual_check && this.scopeElementCount(de.rules_manual_checks)) ||
              (this.show_pass && de.rules_passed.length) ||
              (this.show_hidden && de.rules_hidden.length)) {

            off_screen_elements.push(cache_item);

            v += (de.rules_violations.length > 0) ? de.rules_violations.length : 0;
            w += (de.rules_warnings.length > 0) ? de.rules_warnings.length : 0;
            p += (this.show_pass && de.rules_passed.length > 0) ? de.rules_passed.length : 0;
            m += (this.show_page_manual_check && de.rules_manual_checks.length > 0) ?
                 de.rules_manual_checks.length : 0;
            h += (this.show_hidden && de.rules_hidden.length) ? de.rules_hidden.length : 0;
          }

        }
        else {

          var mc = 0;
          var p = 0;
          var h = 0;

          if (this.show_page_manual_check)
            mc += this.scopePageCount(de.rules_manual_checks);
          if (this.show_element_manual_check)
            mc += this.scopeElementCount(de.rules_manual_checks);
          if (this.show_pass)
            p = de.rules_pass;
          if (this.show_hidden)
            h = de.rules_hidden;

          this.insertDIV(document, node, tag_name, style,
                         de.rules_violations, de.rules_warnings,
                         mc, p, h);
        }
      }
    } //end for

    if (off_screen_elements.length > 0)
      this.showOffScreenCacheItems(off_screen_elements, v, w, m, pp, hh );
  },

  /**
   * @function highlightNodeResults
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Highlights a set of nodes on a page, uses the node result value
   *       for highlighting style
   *
   * @param {Object} document - the DOM to apply highlight operation
   * @param {Array} node_results  - An array of OAA node results to highlight
   *                (i.e. from rule results of filtered rule results)
   */

  highlightNodeResults: function (document, node_results) {

    var RESULT_VALUE = OpenAjax.a11y.RESULT_VALUE;
    var VISIBILITY   = OpenAjax.a11y.VISIBILITY;

    // counters
    var v = 0; // violations
    var w = 0; // warnings
    var m = 0; // manual checks
    var p = 0; // passes
    var h = 0; // hidden

    this.removeHighlight(document);

    if (typeof node_results !== 'object') return;

    // if not an array assume it is a node result object
    if (typeof node_results.length !== 'number') node_results = [node_results];

    var node_results_len = node_results.length;
    var off_screen_elements = [];

    for (var i = 0; i < node_results_len; i++) {

      var node_result = node_results[i];
      var cache_item  = node_result.getCacheItem();
      if (typeof cache_item !== 'object') continue;

      // Get DOMElement object: If cache_item does not have dom_element property
      // referencing an object, then cache_item is itself a DOMElement.
      var dom_element = (typeof cache_item.dom_element === 'object') ?
                        cache_item.dom_element : cache_item;
  
      // If DOMElement type is TEXT_NODE then use its parent element, since
      // that is where its computed style and tag name would come from.
      if (dom_element.type === Node.TEXT_NODE)
        dom_element = dom_element.parent_element;

      var computed_style = dom_element.computed_style;
      var tag_name       = dom_element.tag_name;

      // The node property of DOMElement is a reference to the DOM node in
      // the actual DOM of the current web page.
      var node = dom_element.node;
  
      if (node) {

        // check if the node is off-screen or hidden from assistive technologies
        if (computed_style.is_visible_onscreen === VISIBILITY.HIDDEN) {

          // always do this...
          off_screen_elements.push(node_result);

          // then increment corresponding counter
          switch (node_result.getResultValue()) {
            case RESULT_VALUE.VIOLATION:
              v += 1;
              break;

            case RESULT_VALUE.WARNING:
              w += 1;
              break;

            case RESULT_VALUE.MANUAL_CHECK:
              if (this.show_page_manual_check) m += 1;
              break;

            case RESULT_VALUE.PASS:
              if (this.show_pass) p += 1;
              break;

            case RESULT_VALUE.HIDDEN:
              if (this.show_hidden) h += 1;
              break;

            default:
              break;
          }
        }
        else {
          switch (node_result.getResultValue()) {
            case RESULT_VALUE.VIOLATION:
              this.insertDIV(document, node, tag_name, this.STYLES.violations, 1, 0, 0, 0, 0);
              break;

            case RESULT_VALUE.WARNING:
              this.insertDIV(document, node, tag_name, this.STYLES.warnings, 0, 1, 0, 0, 0);
              break;

            case RESULT_VALUE.MANUAL_CHECK:
              if (this.show_page_manual_check && node_result.isScopePage())
                this.insertDIV(document, node, tag_name, this.STYLES.manualChecks, 0, 0, 1, 0, 0);
              if (this.show_element_manual_check && node_result.isScopeElement())
                this.insertDIV(document, node, tag_name, this.STYLES.manualChecks, 0, 0, 1, 0, 0);
              break;

            case RESULT_VALUE.PASS:
              if (this.show_pass)
                this.insertDIV(document, node, tag_name, this.STYLES.passes, 0, 0, 0, 1, 0);
              break;

            case RESULT_VALUE.HIDDEN:
              if (this.show_hidden)
                this.insertDIV(document, node, tag_name, this.STYLES.hidden, 0, 0, 0, 0, 1);
              break;

            default:
              break;
          }
        }
      }
    } //end for
    if (off_screen_elements.length > 0)
      this.showOffScreenNodeResults(document, off_screen_elements, v, w, m, p, h);
  },

  /**
   * @function removeHighlight
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc unhighlights the nodes that are highlighted earlier and
   *       removes the informational message added to a page if the
   *       element is hidden from Assistive technologies
   *
   * @param {Object}  document  - the DOM to apply highlight operaton
   */
  removeHighlight: function(document) {

    function removeFromDocument(document) {
      var elements = document.getElementsByClassName('oaa_web_accessibility_highlight');

      while (elements[0]) {
        var element = elements[0];

        if (element) {
          var parent_node = element.parentNode;

          while(element.firstChild) {
            parent_node.insertBefore(element.firstChild, element);
          }

          parent_node.removeChild(element);
        }
      }

      // remove highlighting inside iframes
      var iframes = document.getElementsByTagName( "iframe" );

      for (var i = 0; i < iframes.length; i++) {
        var doc = iframes[i].contentDocument;
        // OAA_AE.logger.debug("REMOVE HIGHLIGHT FRAME: " + iframes[i] + " " + doc.location.href);
        if (doc) removeFromDocument(doc);
      }

    }

    function removeFromFrames(frames) {

      if (typeof frames !== 'object' || typeof frames.length !== 'number') return;

      for (var i=0; i < frames.length; i++) {
        var frame = frames[i];
        if (frame.document) removeFromDocument(frame.document);
        removeFromFrames(frame.frames);
      }
    }

    removeFromDocument(document);  //unhighlighting in the actual document

    var frames = window.frames;
    removeFromFrames(frames);

    var off_screen_elements = document.getElementsByClassName('oaa_web_accessibility_off_screen');

    for (var j = 0; j < off_screen_elements.length; j++) {
      if (off_screen_elements[j])
        document.body.removeChild(off_screen_elements[j]);
    }

  },

  /**
   * @function showOffScreenCacheItems
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Display information about a list of cache item results using the
   *       worst result value of each cache item's node results
   *       Designed to provide information about cache items that are not
   *       visible on screen
   *
   * @param {Object} document - the DOM to remove highlighting from
   * @param {Array} cache_items - list of cache items or dom element objects
   * @param {Number} v - number of total violations in all the rules
   *                     associated with all the cache_items
   * @param {Number} w - number of total warnings in all the rules associated
   *                     with all the cache_items
   * @param {Number} m - number of total manual checks in all the rules
   *                     associated with all the cache_items
   * @param {Number} p - number of total pass in all the rules associated with
   *                     all the cache_items
   * @param {Number} h - number of total hidden in all the rules associated
   *                     with all the cache_items
   */
  showOffScreenCacheItems: function (document, cache_items, v, w, m, p, h) {

    if (!document) return;

    var len = cache_items.length;
    var style = this.STYLES.offScreen + ' ';
    var eval_result, tag_name, message;

    if (v > 0) {
      style += this.STYLES.violations;
      eval_result = PluralForm.get(len, this.STRINGS.violations);
    } else if (w > 0) {
      style += this.STYLES.warnings;
      eval_result = PluralForm.get(len, this.STRINGS.warnings);
    } else if (m > 0) {
      style += this.STYLES.manualChecks;
      eval_result = PluralForm.get(len, this.STRINGS.manualChecks);
    } else if (p > 0) {
      style += this.STYLES.passed;
      eval_result = PluralForm.get(len, this.STRINGS.passes);
    } else if (h > 0) {
      style += this.STYLES.hidden;
      eval_result = PluralForm.get(len, this.STRINGS.hidden);
    }

    if (len === 1) {
      if (cache_items[0].dom_element)
        tag_name = cache_items[0].dom_element.tag_name;
      else
        tag_name = cache_items[0].tag_name;

      message = tag_name + ' ' + this.STRINGS.notVisible + ' (' + eval_result + ')';
    }
    else {
      message = len + ' ' + PluralForm.get(len, this.STRINGS.offScreen);
    }

    this.positionDIV(document, style, message);
  },

  /**
   * @function showOffScreenNodeResults
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Display information about a list of node results of a rule
   *       Designed to provide information about node results that are not
   *       visible on screen
   *
   * @param {Object} document - the DOM to remove highlighting from
   * @param {Array} node_results - List of node result objects that are off screen
   * @param {Number} v - number of violations in th list of node results
   * @param {Number} w - number of warnings in the list of node results
   * @param {Number} m - number of manual checks in the list of node results
   * @param {Number} p - number of passes in the list of node results
   * @param {Number} h - number of hidden in the list of node results
   */
  showOffScreenNodeResults: function (document, node_results, v, w, m, p, h) {

    if (!document) return;

    var node_results_plural =
      PluralForm.get(node_results.length, this.STRINGS.offScreen);
    var str = node_results.length + ' ' + node_results_plural;
    var style = this.STYLES.offScreen + ' ';

    if (v > 0) style += this.STYLES.violations;
    else if (w > 0) style += this.STYLES.warnings;
    else if (m > 0) style += this.STYLES.manualChecks;
    else if (p > 0) style += this.STYLES.passes;
    else if (h > 0) style += this.STYLES.hidden;

    if (v > 0) str +=  ': ' + v + ' ' + PluralForm.get(v, this.STRINGS.violations);
    if (w > 0) str +=  ': ' + w + ' ' + PluralForm.get(w, this.STRINGS.warnings);
    if (m > 0) str +=  ': ' + m + ' ' + PluralForm.get(m, this.STRINGS.manualChecks);
    if (p > 0) str +=  ': ' + p + ' ' + PluralForm.get(p, this.STRINGS.passes);
    if (h > 0) str +=  ': ' + h + ' ' + PluralForm.get(h, this.STRINGS.hidden);

    this.positionDIV(document, style, str);
  },

  /**
   * @function positionDIV
   *
   * @desc creates a div to position on top of the web page to show the
   *       off-screen elements and their severities outlined with the worst
   *       result value styling
   *
   * @param {Object}  document  - the DOM to to position DIV
   * @param {String} style - styles the DIV with a border
   * @param {String} result_value_message - message to show inside the DIV
   */
  positionDIV : function (document, style, result_value_message) {

    if (!document.createElement) return;
    var div_element = document.createElement('div');
    div_element.id = 'oaa_web_accessibility_off_screen_id';

    div_element.setAttribute("class", 'oaa_web_accessibility_off_screen');
    div_element.setAttribute("style", style);

    var text_node = document.createTextNode(result_value_message);
    div_element.appendChild(text_node);

    document.body.insertBefore(div_element,document.body.childNodes[0]);
  },

  /**
   * @function insertDIV
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc position the DIV
   *   1. as only child of elements that can have content (e.g., p, h1, h2, li...)
   *   2. as th eparent of elements that are
   *      * defined as empty (e.g., img, input)
   *      * can only contain element (e.g., ul, dl, ol, object...)
   *
   * @param {Object}  document    - the DOM to highlight elements
   * @param {Object}  cache_item
   * @param {Object}  node
   * @param {String}  tag_name
   */
  insertDIV: function (document, node, tag_name, style,
                       violations, warnings, manual_checks, pass, hidden) {

    var flag = false;
    var parent_node = null;
    var elements_without_content = [
      'applet',
      'area',
      'dl',
      'frame',
      'img',
      'input',
      'object',
      'ol',
      'select',
      'textarea',
      'table',
      'ul'
    ];
    var length =  elements_without_content.length;
    var title = this.getResultValueMessage(violations, warnings, manual_checks, pass, hidden);
    var new_div_element = document.createElement('div');

    new_div_element.setAttribute("class", "oaa_web_accessibility_highlight");
    new_div_element.setAttribute("style", style);
    new_div_element.setAttribute("title", title);

    var index = elements_without_content.indexOf(tag_name);

    if (index != -1) {
      parent_node = node.parentNode;
      parent_node.insertBefore(new_div_element, node);
      new_div_element.appendChild(node);
    } else {

      while (node.firstChild) {
        new_div_element.appendChild(node.firstChild);
      }
      node.appendChild(new_div_element);
    }


//  If true, element is aligned with top of scroll area.
//  If false, it is aligned with bottom.
    node.scrollIntoView(true);
  },

  /**
   * @function getWorstResultValueStyle
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Returns a CSS formatted string to be used with the style property of a DOM node
   *       Result is based on the worst node result value associated with a cache item
   *
   *       manual check (red dotted border)
   *       pass (green single border)
   *       violation (red double border)
   *       warning (yellow single border)
   *       hidden (gray double border)
   *       no results (black border)
   *
   * @param {Object} dom_element  -  OAA DOM element object
   * @param {Object} preferences  -  Preferences object from preferences module
   *
   * @return {String} Returns a CSS string that can be used with a style property of a DOM node
   */
  getWorstResultValueStyle: function(dom_element) {

    if (dom_element.rules_violations.length > 0)
      return this.STYLES.violations;
    else if (dom_element.rules_warnings.length  > 0 )
      return this.STYLES.warnings;
    else if (this.show_page_manual_check  && (this.scopePageCount(dom_element.rules_manual_checks) > 0))
      return this.STYLES.manualChecks;
    else if (this.show_element_manual_check && (this.scopeElementCount(dom_element.rules_manual_checks) > 0))
      return this.STYLES.manualChecks;
    else if (this.show_pass &&  (dom_element.rules_passed.length  > 0))
      return this.STYLES.passes;
    else if (this.show_hidden &&  (dom_element.rules_hidden.length  > 0))
      return this.STYLES.hidden;
    else
      return this.STYLES.result;

  },

  /**
   * @function getResultValueMessage
   *
   * @memberOf OAA_WEB_ACCESSIBILITY.util.highlightModule
   *
   * @desc Create value for title attribute of div element to provide summary
   *       of number of violations, warnings, manual checks and passes
   *
   * @param {Number} v_count  - Number of violations
   * @param {Number} w_count  - Number of warnings
   * @param {Number} mc_count - Number of manual checks
   * @param {Number} p_count  - Number of pass
   * @param {Number} h_count  - Number of hidden
   *
   * @return {String} title
   */
  getResultValueMessage: function(v_count, w_count, mc_count, p_count, h_count) {

  var title = '';

    if (v_count > 0)
      title += v_count + ' ' + PluralForm.get(v_count, this.STRINGS.violations) + ' ';

    if (mc_count > 0)
      title += mc_count + ' ' + PluralForm.get(mc_count, this.STRINGS.manualChecks) + ' ';

    if (w_count > 0)
      title += w_count + ' ' + PluralForm.get(w_count, this.STRINGS.warnings) + ' ';

    if (p_count > 0)
      title += p_count + ' ' + PluralForm.get(p_count, this.STRINGS.passes) + ' ';

    if (h_count > 0)
      title += h_count + ' ' + PluralForm.get(h_count, this.STRINGS.hidden) + ' ';

    if (title.length === 0)
      title = this.STRINGS.noResults;

    return title;
  }

};
